Skip to content

Report the outcome of reminders and shares to notes instead of guessing it - #6717

Draft
AndyScherzinger wants to merge 7 commits into
feat/noid/optimisticConversationActionsfrom
feat/noid/optimisticReminders
Draft

AndyScherzinger wants to merge 7 commits into
feat/noid/optimisticConversationActionsfrom
feat/noid/optimisticReminders

Conversation

@AndyScherzinger

@AndyScherzinger AndyScherzinger commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Stacked on #6715 — review that one first. #6694 merged already; this branch targets feat/noid/optimisticConversationActions and will be retargeted to master once #6715 lands.

Two things in the chat reported the wrong outcome, and both sat behind the last RxJava subscriptions in ChatViewModel that still used the disposableSet the chat throws away on pause.

refactor(chat): move reminders and sharing to notes off RxJava — setReminder, getReminder, deleteReminder, shareToNotes and shareLocationToNotes move down through ChatNetworkDataSource and RetrofitChatNetwork onto four new NcApiCoroutines endpoints. This is not the pure refactor it looks like: disposableSet is disposed in onPause, not in onCleared, so backgrounding the chat while one of these requests was in flight cancelled it silently. On viewModelScope they finish.

getCapabilities, joinRoom and createRoom stay on RxJava on purpose. They sit in that same disposableSet, so moving them is the same behaviour change — and for a join or a capability fetch, what should happen when the chat is backgrounded mid-request is a decision, not a refactor. Neither blocks anything.

fix(chat): tell the user when a reminder or a share to notes failed — a reminder the server refused was written to the log and nowhere else. And showSnackBar fires "Message sent" the moment a share to notes is started, before the answer arrives, so a share that failed was reported to the user as a success. Both now report the failure, through the SharedFlow and snackbar shape #6694 established for reaction failures.

A share that failed for a transient reason is retried, but never blindly. Talk stores a message's referenceId and never looks at it again — verified against a Nextcloud 36 dev instance with Talk main: posting twice with the same reference id returns 201 twice and leaves two messages in the room. So before a second attempt the client asks the note to self whether the message arrived anyway, and only sends it again when it did not. A lookup that cannot be answered counts as "it may be there", because a duplicate note is worse than a missing retry. The location share now carries a reference id too, which the share endpoint has always accepted.

Deliberately without an optimistic reminder state, although the implementation plan asked for one: getReminderExistState has exactly one consumer, the reminder dialog header, and DateTimeCompose refetches the reminder in its constructor every time that dialog opens, while Set and Delete dismiss it immediately. An optimistic value would never be rendered.

🖼️ Screenshots

No UI was added or changed. One new string, nc_message_not_added_to_notes, shown in the existing chat snackbar.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed — four tests cover holdsShare, the rule that decides whether a failed share is sent again. ChatViewModel takes 14 constructor dependencies plus sharedApplication, so there is no view-model-level test for the snackbar itself; the helper contract these paths rely on is covered in Put every optimistic action on one helper and fix the ones that failed silently #6715
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed — not backported, this goes into 25.1.0
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch 2 times, most recently from 1cff49c to 93bea38 Compare September 18, 2026 07:38
@AndyScherzinger AndyScherzinger added this to the 25.1.0 milestone Sep 18, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 5099081 to f8ce16e Compare September 24, 2026 14:24
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 93bea38 to 0e97e99 Compare September 24, 2026 14:27
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from f8ce16e to 36e1a8e Compare September 24, 2026 15:01
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 0e97e99 to 0f19149 Compare September 24, 2026 15:01
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 36e1a8e to ba3d4b7 Compare September 24, 2026 16:32
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 0f19149 to bf571a9 Compare September 24, 2026 16:41
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from ba3d4b7 to ca9e10d Compare October 1, 2026 18:59
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from bf571a9 to a0740e5 Compare October 1, 2026 19:02
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch 2 times, most recently from 75b1f11 to fdb65eb Compare October 2, 2026 16:55
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from a0740e5 to 08df497 Compare October 2, 2026 16:57
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from fdb65eb to 4db6b67 Compare October 5, 2026 22:29
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 08df497 to 00d3a61 Compare October 5, 2026 22:32
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 4db6b67 to 77b4de6 Compare October 5, 2026 23:20
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 00d3a61 to 8200d2c Compare October 5, 2026 23:20
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 77b4de6 to 24c3354 Compare October 5, 2026 23:45
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 8200d2c to 57e7e04 Compare October 5, 2026 23:48
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 24c3354 to 007ce89 Compare October 6, 2026 05:48
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 57e7e04 to 2fb50b7 Compare October 6, 2026 05:50
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 007ce89 to 18a84d3 Compare October 6, 2026 08:56
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 2fb50b7 to 85883e2 Compare October 6, 2026 08:59
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 18a84d3 to 43377cb Compare October 10, 2026 16:03
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from 85883e2 to c7fd3fb Compare October 10, 2026 16:05
@AndyScherzinger

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

📝 Walkthrough

Walkthrough

The API and network data source add coroutine operations for reminders and Notes sharing. ChatViewModel uses these operations, checks recent messages after transient sharing failures, and retries once when the lookup confirms the share is absent. It reports action failures through a flow that ChatActivity displays in a snackbar. Tests cover the reference lookup helper.


Priority: ➖ Normal

Merge Risk: 🔵 Low · up to c7fd3

A Notes share may not complete even though the chat shows “Message sent,” and an uncertain outcome is not reported. Correct the lookup and feedback paths before merging unless that bounded risk is accepted.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 6 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: reporting actual outcomes for reminders and shares to notes.
Description check Passed The description explains the motivation, implementation, retry behavior, testing, and checklist status. It omits the template's TODO section, but the description is otherwise complete.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

Full details: Docstring Coverage

Explanation

Docstring coverage is 12.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 6 files. (1 skipped: 1 unsupported.)


  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fbe4d5e8-5473-4715-bd02-27cbbd313487
📥 Commits

Reviewing files that changed from the base of the PR and between 43377cb and c7fd3fb.

📒 Files selected for processing (7)
  • app/src/main/java/com/nextcloud/talk/api/NcApiCoroutines.kt
  • app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  • app/src/main/java/com/nextcloud/talk/chat/data/network/ChatNetworkDataSource.kt
  • app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt
  • app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
  • app/src/main/res/values/strings.xml
  • app/src/test/java/com/nextcloud/talk/chat/viewmodels/ChatViewModelTest.kt

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

Comment on lines +2178 to +2180
shareToNotes("share the location to notes", credentials, url) { referenceId ->
chatNetworkDataSource.shareLocationToNotes(credentials, url, objectType, objectId, metadata, referenceId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'shareLocationToNotes|shareToNotes\(|getUrlToSendLocation|getUrlForChat|alreadyShared|pullChatMessages' app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt app/src/main/java/com/nextcloud/talk/api/ApiUtils.kt
sed -n '2150,2252p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Repository: nextcloud/talk-android

Length of output: 9236


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- ApiUtils declarations ---'
rg -n -F --glob '*.kt' -- 'fun getUrlToSendLocation' .
rg -n -F --glob '*.kt' -- 'fun getUrlForChat(' .
printf '%s\n' '--- URL helper implementations ---'
for f in $(rg -l -F --glob '*.kt' -- 'fun getUrlToSendLocation' .); do
  file="$f"
  start=$(rg -n -F -- 'fun getUrlToSendLocation' "$file" | cut -d: -f1 | head -1)
  end=$((start + 22))
  sed -n "${start},${end}p" "$file"
done
for f in $(rg -l -F --glob '*.kt' -- 'fun getUrlForChat(' .); do
  file="$f"
  start=$(rg -n -F -- 'fun getUrlForChat(' "$file" | cut -d: -f1 | head -1)
  end=$((start + 18))
  sed -n "${start},${end}p" "$file"
done
printf '%s\n' '--- ChatActivity location-share caller ---'
sed -n '3870,3955p' app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
printf '%s\n' '--- Retrofit data-source methods and API declarations ---'
sed -n '80,155p' app/src/main/java/com/nextcloud/talk/chat/data/network/RetrofitChatNetwork.kt
rg -n -F --glob '*.kt' -- 'shareLocationToNotes' app/src/main/java
rg -n -F --glob '*.kt' -- 'pullChatMessages' app/src/main/java/com/nextcloud/talk/chat/data/network

Repository: nextcloud/talk-android

Length of output: 10182


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- coroutine API bindings ---'
rg -n -F --glob '*.kt' -- 'fun sendLocation' app/src/main/java
rg -n -F --glob '*.kt' -- 'fun pullChatMessages' app/src/main/java
printf '%s\n' '--- API declaration context ---'
for f in $(rg -l -F --glob '*.kt' -- 'fun sendLocation' app/src/main/java); do
  line=$(rg -n -F -- 'fun sendLocation' "$f" | head -1 | cut -d: -f1)
  sed -n "$((line-8)),$((line+14))p" "$f"
done
for f in $(rg -l -F --glob '*.kt' -- 'fun pullChatMessages' app/src/main/java); do
  line=$(rg -n -F -- 'fun pullChatMessages' "$f" | head -1 | cut -d: -f1)
  sed -n "$((line-8)),$((line+18))p" "$f"
done
printf '%s\n' '--- holdsShare and transient retry flow ---'
rg -n -F -- 'fun holdsShare' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
sed -n '2190,2265p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Repository: nextcloud/talk-android

Length of output: 7481


🏁 Script executed:

sed -n '2718,2745p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt

Repository: nextcloud/talk-android

Length of output: 1533


Use the Notes chat URL for the delivery lookup.

getUrlToSendLocation appends /share to the chat URL. ChatActivity passes that URL to shareLocationToNotes, and shareToNotes reuses it for alreadyShared. The lookup therefore sends pullChatMessages to the location-share endpoint instead of the note-to-self chat endpoint.

The lookup can fail or return no message data. alreadyShared and holdsShare treat both cases as possibly delivered, so the retry is skipped. The user may then see only the immediate “Message sent” snackbar.

Pass the Notes chat URL as a separate lookup URL. Keep the /share URL for the location-share request.

Comment on lines +2205 to +2207
if (!alreadyShared(credentials, notesUrl, referenceId)) {
attemptShare(share, referenceId)?.let { reportShareFailure(description, it) }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

An inconclusive lookup ends the flow without telling the user.

If alreadyShared returns true because the lookup failed or returned 304, the coroutine ends with no emission. The share may not have arrived, but the chat has already shown "Message sent". The PR goal is to report the outcome instead of guessing it. Skipping the retry is correct here, but the result is still uncertain. Report that uncertainty, for example with the existing failure string or a "may not have been added" string, when the lookup could not confirm the share.

Comment on lines +2260 to +2269
@Suppress("Detekt.TooGenericExceptionCaught")
private fun requestWithFeedback(description: String, @StringRes failureMessage: Int, request: suspend () -> Unit) {
viewModelScope.launch {
try {
request()
} catch (e: Exception) {
logger.e(TAG, "Failed to $description", e)
_actionFailures.tryEmit(failureMessage)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1855,1905p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
sed -n '2240,2275p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
sed -n '1520,1550p' app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Repository: nextcloud/talk-android

Length of output: 5273


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- imports and relevant declarations ---'
sed -n '1,90p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
sed -n '1860,1900p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
sed -n '2248,2275p' app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt
printf '%s\n' '--- reminder call sites ---'
rg -n -F --glob '*.kt' -- 'getReminder(' app/src/main/java
rg -n -F --glob '*.kt' -- 'setReminder(' app/src/main/java
rg -n -F --glob '*.kt' -- 'deleteReminder(' app/src/main/java
printf '%s\n' '--- actionFailures collector owner and lifecycle context ---'
rg -n -F --glob 'ChatActivity.kt' -- 'actionFailures' app/src/main/java
sed -n '1470,1575p' app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
printf '%s\n' '--- view-model scope and activity lifecycle declarations ---'
rg -n -F --glob '*.kt' -- 'class ChatActivity' app/src/main/java
rg -n -F --glob '*.kt' -- 'onCleared(' app/src/main/java/com/nextcloud/talk/chat

Repository: nextcloud/talk-android

Length of output: 16110


Preserve cancellation in both reminder handlers.

Both handlers catch CancellationException through Exception. requestWithFeedback can report cancellation through _actionFailures, while getReminder resets the state to GetReminderStartState. The snackbar is conditional because ChatActivity collects the flow in lifecycleScope; it may not appear if the activity is being destroyed. The cancellation is still incorrectly swallowed.

Suggested fix
+            } catch (e: CancellationException) {
+                throw e
             } catch (e: Exception) {

Add this cancellation-first catch before the existing Exception catch in both requestWithFeedback and getReminder.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Suppress("Detekt.TooGenericExceptionCaught")
private fun requestWithFeedback(description: String, @StringRes failureMessage: Int, request: suspend () -> Unit) {
viewModelScope.launch {
try {
request()
} catch (e: Exception) {
logger.e(TAG, "Failed to $description", e)
_actionFailures.tryEmit(failureMessage)
}
}
@Suppress("Detekt.TooGenericExceptionCaught")
private fun requestWithFeedback(description: String, @StringRes failureMessage: Int, request: suspend () -> Unit) {
viewModelScope.launch {
try {
request()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
logger.e(TAG, "Failed to $description", e)
_actionFailures.tryEmit(failureMessage)
}
}

Four call sites had each grown their own copy of "apply locally, retry once on a
transient failure, revert when it finally fails, revert when cancelled", with the
retry predicate spelled out twice and one of them shipping a guard bug.

optimisticAction() now carries that shape, including the case where the server
refuses in the payload rather than in the status code, and the case where an
answer means the change was already applied - which must not revert anything.

No behaviour change; the repository tests of reactions, deletions, edits, pinning
and dismissals carry the migration.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Marking a conversation read or unread, favoriting, archiving and the
important and sensitive toggles now go through the shared optimistic
helper, so every one of them retries a connection problem once and takes
its change back when the screen is left mid-request.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The notification level, the message expiration and the call notification
switch logged their failure and left the screen showing a setting the
server never accepted. They now go back to the previous value and say so.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
… once

Deleting left the row in place until the worker had finished and the list
had been fetched again. It now disappears on confirmation, reusing the
guard that hides a conversation being left, and comes back if the delete
fails.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The helper was only exercised through the repositories that use it, so the
rules every optimistic action depends on - applied before the answer, taken
back on a refusal or a cancellation, kept after one hiccup - had no test of
their own.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Both used an Observable with a disposable that the chat throws away when it
is paused, so backgrounding the app while the request was in flight dropped
it silently. They now run in the view model scope like every other request
that has to finish.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
A reminder the server refused was only logged, and the chat says "Message
sent" the moment a share to notes starts, before the answer arrives - so a
share that failed was reported as a success. Both now show what happened.

Assisted-by: Claude Code:claude-opus-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 43377cb to a238355 Compare October 11, 2026 09:14
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticReminders branch from c7fd3fb to dcc3a79 Compare October 11, 2026 09:14
@mahibi
mahibi force-pushed the feat/noid/optimisticConversationActions branch from a238355 to 09db8f2 Compare October 12, 2026 13:42
@AndyScherzinger
AndyScherzinger force-pushed the feat/noid/optimisticConversationActions branch from 09db8f2 to c43e42d Compare October 12, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant